"""
    xfree.utils.init
    ~~~~~~~~~~~~~~~~~~~~~
    文件描述：初始化系统函数
    作者：xfree
    邮箱：cy_83k@qq.com
"""
import os
import sys
import time
from datetime import datetime
from xfree.display.navigation import NavigationContentType
from xfree.extensions import (
    alembic,
    allows,
    babel,
    cache,
    celery,
    csrf,
    db,
    debugtoolbar,
    limiter,
    login_manager,
    mail,
    redis_store,
    themes,
    whooshee,
)
from xfree.utils.requirements import (
    IsAdmin,
    IsAtleastModerator,
    CanEditUser,
    CanBanUser,
    permission_with_identity,
    can_moderate,
    can_post_reply,
    can_edit_post,
    can_post_topic,
    can_delete_topic,
    has_permission)
from xfree.utils.search import (
    ForumWhoosheer,
    PostWhoosheer,
    TopicWhoosheer,
    UserWhoosheer,
)
from sqlalchemy.exc import OperationalError, ProgrammingError
from xfree.plugins import spec
from xfree.plugins.manager import XFreePluginManager
from xfree.plugins.models import PluginRegistry
from xfree.utils.deprecation import XFreeDeprecation
from xfree.utils.helpers import get_xfree_config, app_config_from_env, crop_title, format_date, format_time, \
    format_datetime, forum_is_unread, is_online, time_since, topic_is_unread, time_utcnow, mark_online, render_template, \
    get_alembic_locations
from sqlalchemy import event
from sqlalchemy.engine import Engine
from flask_login import current_user
import logging
import logging.config
import warnings
# models
from xfree.user.models import Guest, User
from xfree.utils.plugin import remove_zombie_plugins_from_db, template_hook
from xfree.utils.settings import xfree_config
from xfree.utils.translations import XFreeDomain
from flask import Flask, request, current_app

logger = logging.getLogger(__name__)


def init_config(app, config):
    """配置 xfree。
    :param app: Flask 应用实例
    :param config: 配置对象或配置文件路径
    """
    # 1.加载默认配置到 app.config
    try:
        app.config.from_object("xfree.configs.default.DefaultConfig")
    except ImportError:
        print("加载系统默认配置文件失败，系统即将退出！")
        exit()

    # 2. 加载用户配置到 app.config
    # 2.1获取用户配置文件xfree.cfg的位置
    config = get_xfree_config(app, config)
    # 2.2加载用户配置文件到app.config
    try:
        # 如果config是字符串类型（表示配置文件路径）用from_pyfile加载配置
        if isinstance(config, str):
            app.config.from_pyfile(config)
        else:
            # 如果config是对象用from_object加载配置
            app.config.from_object(config)
    except ImportError:
        print("加载用户配置文件失败，系统将使用默认配置！")
    # 2.3将获取配置文件路径并加载配置
    app.config["CONFIG_PATH"] = config

    #  3.从环境变量加载配置
    # 3.1从环境变量加载配置,如果环境变量不存在则不加载
    app.config.from_envvar("XFREE_SETTINGS", silent=True)
    # 3.2从环境变量加载特定配置项（以 XFREE_ 开头的环境变量）
    app_config_from_env(app, prefix="XFREE_")

    # 4. 迁移 Celery 4.x 配置到 Celery 6.x
    # 4.1获取旧格式的 Celery 配置
    old_celery_config = app.config.get_namespace("CELERY_")
    # 4.2将旧格式的配置项转换为新格式的配置项
    celery_config = {}
    for key, value in old_celery_config.items():
        # 新格式的配置项不是 config
        if key!= "config":
            config_key = f"CELERY_{key.upper()}"
            celery_config[key] = value
            try:
                del app.config[config_key]
            except KeyError:
                pass
    # 4.3将新格式的配置项更新到 app.config
    new_celery_config = app.config.get("CELERY_CONFIG")
    new_celery_config.update(celery_config)
    app.config.update({"CELERY_CONFIG": new_celery_config})

    # 5. 配置日志记录功能
    # 5.1 加载日志配置
    configure_logging(app)
    # 5.2 在日志记录中使用config的完整名称，以便于调试和分析
    if not isinstance(config, str) and config is not None:
        config_name = "{}.{}".format(config.__module__, config.__name__)
    else:
        config_name = config
    # 5.3 将配置文件路径记录到日志中
    logger.info("使用的配置来自：{}".format(config_name))
    deprecation_level = app.config.get("DEPRECATION_LEVEL", "default")
    # 在测试时永远不设置弃用级别，pytest 会处理它
    if not app.testing:  # pragma: no branch
        warnings.simplefilter(deprecation_level, XFreeDeprecation)
    # 5.4 配置调试工具栏
    debug_panels = app.config.setdefault(
        "DEBUG_TB_PANELS",
        [
            "flask_debugtoolbar.panels.versions.VersionDebugPanel",
            "flask_debugtoolbar.panels.timer.TimerDebugPanel",
            "flask_debugtoolbar.panels.headers.HeaderDebugPanel",
            "flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
            "flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel",
            "flask_debugtoolbar.panels.template.TemplateDebugPanel",
            "flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel",
            "flask_debugtoolbar.panels.logger.LoggingPanel",
            "flask_debugtoolbar.panels.route_list.RouteListDebugPanel",
            "flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
        ],
    )
    # 5.5 添加警告面板
    if all("WarningsPanel" not in p for p in debug_panels):
        debug_panels.append("flask_debugtoolbar_warnings.WarningsPanel")

    # 6. 初始化插件管理器
    app.pluggy = XFreePluginManager("xfree")



def init_context_processor(app):
    """
        初始化上下文处理器。
        :param app: Flask 应用对象。
    """

    @app.context_processor
    def inject_xfree_config():
        """
        将 'xfree_config' 配置变量注入到模板中。

        :return: 包含注入变量的字典。
        """
        return dict(xfree_config=xfree_config, format_date=format_date)

    @app.context_processor
    def inject_now():
        """
        将当前时间注入到模板中。

        :return: 包含注入变量的字典。
        """
        return dict(now=datetime.utcnow())


def init_template_filters(app):
    """初始化模板过滤器"""
    # 创建一个空字典，用于存储模板过滤器
    filters = {}

    # 添加裁剪标题的过滤器函数
    filters["crop_title"] = crop_title
    # 添加格式化日期的过滤器函数
    filters["format_date"] = format_date
    # 添加格式化时间的过滤器函数
    filters["format_time"] = format_time
    # 添加格式化日期时间的过滤器函数
    filters["format_datetime"] = format_datetime
    # 添加判断论坛是否未读的过滤器函数
    filters["forum_is_unread"] = forum_is_unread
    # 添加判断用户是否在线的过滤器函数
    filters["is_online"] = is_online
    # 添加计算时间间隔的过滤器函数
    filters["time_since"] = time_since
    # 添加判断主题是否未读的过滤器函数
    filters["topic_is_unread"] = topic_is_unread

    # 定义权限列表，包含权限名称和对应的权限类
    permissions = [
        ("is_admin", IsAdmin),
        ("is_moderator", IsAtleastModerator),
        ("is_admin_or_moderator", IsAtleastModerator),
        ("can_edit_user", CanEditUser),
        ("can_ban_user", CanBanUser),
    ]

    # 遍历权限列表，将权限类转换为模板过滤器并添加到字典中
    filters.update(
        (name, permission_with_identity(perm, name=name)) for name, perm in permissions
    )

    # 添加判断是否能进行管理操作的过滤器函数
    filters["can_moderate"] = can_moderate
    # 添加判断是否能回复帖子的过滤器函数
    filters["post_reply"] = can_post_reply
    # 添加判断是否能编辑帖子的过滤器函数
    filters["edit_post"] = can_edit_post
    # 删除帖子实际上也是编辑帖子的一种操作，使用相同的过滤器
    filters["delete_post"] = can_edit_post
    # 添加判断是否能发布主题的过滤器函数
    filters["post_topic"] = can_post_topic
    # 添加判断是否能删除主题的过滤器函数
    filters["delete_topic"] = can_delete_topic
    # 添加判断是否具有特定权限的过滤器函数
    filters["has_permission"] = has_permission

    # 将所有定义的过滤器更新到应用程序的 Jinja2 模板环境中
    app.jinja_env.filters.update(filters)

    # 在模板环境中设置全局变量，用于执行模板钩子函数
    app.jinja_env.globals["run_hook"] = template_hook
    # 在模板环境中设置全局变量，用于表示导航内容类型
    app.jinja_env.globals["NavigationContentType"] = NavigationContentType

    # 调用插件钩子，可能用于触发其他插件在 Jinja2 模板中添加自定义指令或功能
    app.pluggy.hook.xfree_jinja_directives(app=app)


def init_blueprint(app):
    app.pluggy.hook.xfree_load_blueprints(app=app)


# 1.初始化配置


def init_celery(app, celery):
    """
    初始化 Celery 多任务管理器。

    参数：
    - app：Flask 应用对象。
    - celery：Celery 对象。

    作用：
    加载 Celery 多任务管理器，将应用的配置信息更新到 Celery 中，并设置任务执行的上下文环境。
    """
    # 1.加载 Celery 配置
    celery.conf.update(app.config.get("CELERY_CONFIG"))

    # 2.设置 Celery 的任务基类为自定义的上下文任务类
    TaskBase = celery.Task

    class ContextTask(TaskBase):
        """
        自定义的 Celery 任务类，用于在任务执行时自动激活应用上下文。

        方法：
        - __call__：在任务被调用时执行的方法。
        """

        def __call__(self, *args, **kwargs):
            """
            在任务执行前，激活应用上下文，确保任务在正确的上下文环境中执行。

            参数：
            - *args：任务的位置参数。
            - **kwargs：任务的关键字参数。

            返回：
            - 调用原始任务基类的 __call__ 方法执行任务，并返回结果。
            """
            with app.app_context():
                return TaskBase.__call__(self, *args, **kwargs)

    # 3.将 Celery 的任务基类设置为自定义的上下文任务类
    celery.Task = ContextTask


def init_extensions(app):
    """
    初始化 Flask 扩展。
    Args:
        app: Flask 应用实例。
    Returns:
        无。
    """
    # 1. Flask-Allows：初始化权限管理扩展
    allows.init_app(app)
    allows.identity_loader(lambda: current_user)

    # 2. Flask-WTF：初始化表单扩展
    csrf.init_app(app)

    # 3. Flask-User：初始化用户管理扩展
    db.init_app(app)

    # 4. Flask-Alembic：初始化数据库迁移扩展
    alembic.init_app(app, command_name="db")

    # 5. Flask-Mail：初始化邮件扩展
    mail.init_app(app)

    # 6. Flask-Cache：初始化缓存扩展
    cache.init_app(app)

    # 7. Flask-DebugToolbar：初始化调试工具栏扩展
    debugtoolbar.init_app(app)

    # 8. Flask-Themes：初始化主题管理扩展，并指定应用标识为"xfree"
    themes.init_themes(app, app_identifier="xfree")

    # 9. Flask-Redis：初始化 Redis 扩展
    redis_store.init_app(app)

    # 10. Flask-Limiter：初始化请求限速扩展
    limiter.init_app(app)

    # 11. Flask-WhooshAlchemy：初始化全文搜索扩展
    whooshee.init_app(app)
    # 如果不是在单元测试中，注册与不同模型相关的搜索处理器，可大大加快测试速度
    if not app.testing:
        whooshee.register_whoosheer(PostWhoosheer)
        whooshee.register_whoosheer(TopicWhoosheer)
        whooshee.register_whoosheer(ForumWhoosheer)
        whooshee.register_whoosheer(UserWhoosheer)

    # 12. Flask-Login：初始化用户认证扩展
    login_manager.login_view = app.config["LOGIN_VIEW"]  # 设置登录视图
    login_manager.refresh_view = app.config["REAUTH_VIEW"]  # 设置刷新视图
    login_manager.login_message_category = app.config["LOGIN_MESSAGE_CATEGORY"]  # 设置登录消息类别
    login_manager.needs_refresh_message_category = app.config["REFRESH_MESSAGE_CATEGORY"]  # 设置需要刷新消息的类别
    login_manager.anonymous_user = Guest  # 设置匿名用户
    # 使用Flask-Login扩展进行用户登录管理的一个片段
    @login_manager.user_loader
    def load_user(user_id):
        """加载用户。是`login`扩展所需的。"""
        user_instance = User.query.filter_by(id=user_id).first()
        if user_instance:
            return user_instance
        else:
            return None

    # 13. Flask-Admin：初始化管理界面扩展
    login_manager.init_app(app)



# 4.插件
def init_plugin(app):
    """
    初始化插件。

    """
    # 1. 创建一个插件规范对象，用于定义插件的钩子函数。
    app.pluggy.add_hookspecs(spec)

    # 2. 获取所有以 "xfree" 开头的模块，这些模块可能包含插件。
    xfree_modules = set(
        module for name, module in sys.modules.items() if name.startswith("xfree")
    )

    # 3. 遍历所有 "xfree" 模块，并尝试将它们注册到插件系统中。
    for module in xfree_modules:
        try:
            app.pluggy.register(module, internal=True)
            print("模块加载成功: ", module.__name__)
        except Exception as e:
            print(print("模块加载失败: ", e))
            exit()

    # 4. 尝试从数据库中获取所有插件记录，并根据这些记录设置插件系统的状态。
    try:
        # 创建一个应用程序上下文环境，确保在这个代码块中可以访问到应用程序的上下文
        with app.app_context():
            # 尝试从数据库中获取所有的插件记录，假设PluginRegistry是一个数据库模型，用于存储插件的相关信息。
            plugins = PluginRegistry.query.all()
            print("插件加载成功: ", plugins)
    # 出现错误时，捕获OperationalError和ProgrammingError异常，并记录调试信息。
    except (OperationalError, ProgrammingError) as exc:
        logger.debug(
            "数据库设置不正确或尚未设置。",
            exc_info=exc,
        )
        # 即使数据库设置不正确，也要加载插件
        # 即，在创建初始数据库并希望安装时
        # 插件迁移也是如此
        # 加载通过 setuptools 入口点定义的插件。这些插件通常在 setup.py 文件中通过 entry_points 参数指定。
        app.pluggy.load_setuptools_entrypoints("xfree_plugins")
        return
    # 5. 遍历所有插件记录，并根据它们的启用状态设置插件系统的状态。
    for plugin in plugins:
        print("插件加载成功: ", plugin)
        if not plugin.enabled:
            # 如果插件未启用，则在插件系统中将其标记为阻塞状态
            app.pluggy.set_blocked(plugin.name)
    # 6. 调用插件系统的钩子函数，传递应用程序对象作为参数。
    # 再次加载通过 setuptools 入口点定义的插件
    app.pluggy.load_setuptools_entrypoints("xfree_plugins")
    # 调用插件系统的钩子，传递应用程序对象作为参数
    app.pluggy.hook.xfree_extensions(app=app)

    # 获取已加载插件的名称集合和已在数据库中注册插件的名称集合
    loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
    registered_names = set([p.name for p in plugins])
    unregistered = [
        PluginRegistry(name=name)
        for name in loaded_names - registered_names
        # 忽略内部的 xfree 模块
        if not name.startswith("xfree.") and name != "xfree"
    ]
    with app.app_context():
        # 将未注册的插件添加到数据库会话中
        db.session.add_all(unregistered)
        # 提交数据库会话，将未注册的插件保存到数据库
        db.session.commit()
        print("插件加载成功: ", unregistered)
        removed = 0
        # 如果配置项 REMOVE_DEAD_PLUGINS 为 True，则移除无效插件
        if app.config["REMOVE_DEAD_PLUGINS"]:
            removed = remove_zombie_plugins_from_db()
            logger.info("删除插件: {}".format(removed))

    # 由于在迭代过程中字典大小可能会改变，会导致 RuntimeError，因此需要复制一份
    tasks = celery.tasks.copy()
    disabled_plugins = [p.__package__ for p in app.pluggy.get_disabled_plugins()]
    for task_name, task in tasks.items():
        print("任务加载成功: ", task_name, task)
        if task.__module__.split(".")[0] in disabled_plugins:
            logger.debug("注销任务: '{}'".format(task))
            # 如果任务所属的模块在禁用插件列表中，则取消注册该任务
            celery.tasks.unregister(task_name)




def init_before_handlers(app):
    """
        配置请求前处理器。

        :param app: Flask 应用对象。
        """

    @app.before_request
    def update_lastseen():
        """
        如果用户已认证，在每次请求前更新用户的'lastseen'（最后一次出现时间）。

        :return: 无返回值，更新数据库中的用户最后出现时间。
        """
        if current_user.is_authenticated:
            current_user.lastseen = time_utcnow()
            db.session.add(current_user)
            db.session.commit()

    if app.config["REDIS_ENABLED"]:

        @app.before_request
        def mark_current_user_online():
            """
            如果用户已认证，将用户标记为在线；如果用户未认证，将用户的 IP 地址标记为在线（作为访客）。

            :return: 无返回值，通过 Redis 将用户或 IP 地址标记为在线状态。
            """
            if current_user.is_authenticated:
                mark_online(current_user.username)
            else:
                mark_online(request.remote_addr, guest=True)

    app.pluggy.hook.xfree_request_processors(app=app)


def init_after_handlers(app):
    pass


def init_error_handlers(app):
    """
        配置错误处理器。

        :param app: Flask 应用实例。
        """

    @app.errorhandler(403)
    def forbidden_page(error):
        """
        处理 403 禁止访问错误。

        :param error: 错误对象。
        :return: 渲染禁止访问页面模板，并返回状态码 403。
        """
        return render_template("errors/forbidden_page.html"), 403

    @app.errorhandler(404)
    def page_not_found(error):
        """
        处理 404 页面未找到错误。

        :param error: 错误对象。
        :return: 渲染页面未找到模板，并返回状态码 404。
        """
        return render_template("errors/page_not_found.html"), 404

    @app.errorhandler(500)
    def server_error_page(error):
        """
        处理 500 服务器内部错误。

        :param error: 错误对象。
        :return: 渲染服务器错误页面模板，并返回状态码 500。
        """
        return render_template("errors/server_error.html"), 500

    app.pluggy.hook.xfree_errorhandlers(app=app)


def init_migrations(app):
    """
    配置数据库迁移。

    :param app: Flask 应用实例。
    """
    # 通过插件系统获取包含数据库迁移脚本的插件目录列表
    plugin_dirs = app.pluggy.hook.xfree_load_migrations()
    # 根据插件目录生成 Alembic 数据库迁移脚本的位置列表
    version_locations = get_alembic_locations(plugin_dirs)

    # 在 Flask 应用的配置中设置 Alembic 的版本位置
    app.config["ALEMBIC"]["version_locations"] = version_locations


def init__translations(app):
        """
        配置翻译功能。

        :param app: Flask 应用实例。
        """

        # 必须在加载插件之后初始化扩展，因为我们使用了'xfree_load_translations'钩子。
        babel.init_app(app=app, default_domain=XFreeDomain(app))

        @babel.localeselector
        def get_locale():
            """
            获取当前用户的语言设置作为区域设置，如果用户未设置则使用默认语言。

            :return: 当前应使用的语言代码。
            """
            # 如果用户已登录且有语言设置，使用用户设置的语言。
            if current_user and current_user.is_authenticated and current_user.language:
                return current_user.language
            # 否则使用默认语言。
            return xfree_config["DEFAULT_LANGUAGE"]


def configure_logging(app):
    """加载日志记录配置：默认日志配置、加载外部日志配置文件以及记录 SQL 查询执行时间
    :param app: Flask应用实例
    """
    # 检查应用配置中是否有USE_DEFAULT_LOGGING这个键，并且其值为True。如果是，就调用configure_default_logging(app)函数来配置默认的日志记录功能。
    if app.config.get("USE_DEFAULT_LOGGING"):
        configure_default_logging(app)
    # 检查应用配置中是否有LOG_CONF_FILE这个键，并且其值不为空。如果是，就调用configure_file_logging(app)函数来配置文件日志功能。
    if app.config.get("LOG_CONF_FILE"):
        logging.config.fileConfig(
            app.config["LOG_CONF_FILE"], disable_existing_loggers=False
        )
    # 检查应用配置中是否有LOG_CONF_DICT这个键，并且其值不为空。如果是，就调用configure_dict_logging(app)函数来配置字典日志功能。
    if app.config["SQLALCHEMY_ECHO"]:
        @event.listens_for(Engine, "before_cursor_execute")
        def before_cursor_execute(
                conn, cursor, statement, parameters, context, executemany
        ):
            conn.info.setdefault("query_start_time", []).append(time.time())

        @event.listens_for(Engine, "after_cursor_execute")
        def after_cursor_execute(
                conn, cursor, statement, parameters, context, executemany
        ):
            total = time.time() - conn.info["query_start_time"].pop(-1)
            app.logger.debug("总时间: %f", total)


def configure_default_logging(app):
    """加载默认日志记录配置"""
    # 根据应用配置中的LOG_DEFAULT_CONF键所对应的字典配置来设置日志记录器、处理器和格式等。这个字典通常包含了日志级别、输出目标、日志格式等信息。
    logging.config.dictConfig(app.config["LOG_DEFAULT_CONF"])

    # 检查应用配置中是否有SEND_LOGS这个键，并且其值为True。如果是，就调用configure_mail_logs(app)函数来配置邮件日志功能。
    if app.config["SEND_LOGS"]:
        configure_mail_logs(app)


def configure_mail_logs(app, formatter):
    """加载邮件日志记录配置
    :param app: Flask应用实例
    :param formatter: 日志格式对象
    """
    from logging.handlers import SMTPHandler
    # 创建一个日志格式对象，指定了日志中包含的时间、日志级别、日志记录器名称和日志消息的格式。
    formatter = logging.Formatter("%(asctime)s %(levelname)-7s %(name)-25s %(message)s")
    # 创建一个SMTPHandler对象，用于将日志消息发送到指定的邮件服务器。这个对象需要传入邮件服务器地址、发件人地址、收件人地址、主题、以及SMTP服务器的用户名和密码。
    mail_handler = SMTPHandler(
        app.config["MAIL_SERVER"],
        app.config["MAIL_DEFAULT_SENDER"],
        app.config["ADMINS"],
        "application error, no admins specified",
        (app.config["MAIL_USERNAME"], app.config["MAIL_PASSWORD"]),
    )
    # 设置日志级别为ERROR，表示只记录错误级别的日志。
    mail_handler.setLevel(logging.ERROR)
    # 将上面创建的日志格式对象设置为SMTPHandler对象的格式。
    mail_handler.setFormatter(formatter)
    # 将SMTPHandler对象添加到应用的日志记录器中，这样应用在运行过程中产生的错误日志就会通过这个处理器发送到指定的邮件地址。
    app.logger.addHandler(mail_handler)
